package main

import (
	"com.opennews.openplatform/web_api/ent"
	"com.opennews.openplatform/web_api/ent/car"
	"com.opennews.openplatform/web_api/ent/group"
	"com.opennews.openplatform/web_api/ent/product"
	"com.opennews.openplatform/web_api/ent/user"
	"com.opennews.openplatform/web_api/shared"
	"context"
	"database/sql"
	"errors"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/spf13/viper"
	"time"
)

type AppConfig struct {
	DataSource struct {
		Dsn string
	}
}

var appConfig AppConfig

func main() {
	viper.SetConfigFile("./conf/application.yaml")
	viper.ReadInConfig()
	viper.Unmarshal(&appConfig)

	//ExecuteRawSql(appConfig)

	// It requires "github.com/go-sql-driver/mysql" during the runtime. That's why we need a blank import for it.
	//
	// IMPORTANT!!!
	// You would encounter error when your model contains time.Time type field.
	// The error occurs during query.
	// sql: Scan error on column index 2, name "registered_at": unsupported Scan, storing driver.Value type []uint8 into type*time.Time
	// parseTime=true parameter fixed this issue.
	//
	// dataSourceName format for mysql: <user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True.
	client, err := ent.Open("mysql", appConfig.DataSource.Dsn)

	if err != nil {
		fmt.Printf("failed opening connection to mysql: %v", err)
	}

	// Ensures client will be closed.
	defer client.Close()

	ctx := context.Background()

	// Runs the auto migration tool. It creates/updates database schema.
	// Runs the migration in debug mode (printing all SQL queries).
	if err := client.Schema.Create(ctx); err != nil {
		fmt.Printf("failed creating schema resources: %v", err)
	}

	//// If you want migration plan to drop column or index.
	//// PROCEED WITH CAUTION IN PRO ENVIRONMENT
	//if err := client.Schema.Create(ctx, migrate.WithDropColumn(true), migrate.WithDropIndex(true)); err != nil {
	//	fmt.Printf("failed creating schema resources: %v", err)
	//}

	//QueryUser(ctx, client)

	const dateFormat = "2006-01-02 15:04:05"

	client.Debug().Product.Query().Where(
		product.HasTags(),
	).AllX(ctx)

	//CreateUser(ctx, client)

	//user := QueryUserByName(ctx, client, "Ariel")
	//birthday, _ := time.Parse(dateFormat, "1995-10-03 10:21:12")
	////user.Update().SetBirthday(birthday).ExecX(ctx)
	//user.Birthday = birthday
	//
	//println(user.Name)

	//QueryGroupWithUsers(ctx, client)
	//CreateUser(ctx, client)

	println("Done")
}

// Creates new user.
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
	// Defines a Time type for testing.
	birthday, _ := time.Parse("2006-01-02", "1986-12-21")

	// Creates a UserCreate instance with client.User.Create function and sets each field. Saves when setting finishes.
	u, err := client.User.
		Create().
		SetAge(21).
		SetName("Json").
		SetNillableBirthday(&birthday).
		SetChecked(time.Now()).
		Save(ctx)

	if err != nil {
		fmt.Printf("failed creating user: %v", err)
		return nil, fmt.Errorf("failed creating user: %v", err)
	}

	fmt.Println("user was created: ", u)

	return u, nil
}

// Queries user.
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
	// Creates a UserQuery instance with client.User.Query() function and sets the where condition.
	// If you want to check on the generated sql just use client.Debug().User instead.
	u, err := client.User.
		Query().
		Where(user.NameEQ("a8m")).
		Only(ctx)

	// region: Selects return fields.
	//
	//// Only queries one field.
	//names, _ := client.User.Query().Select(user.FieldName).Strings(ctx)
	//
	//// Defines the result list of user structure.
	//matched := []ent.User{}
	//
	//// Queries user data and selects needed fields only.
	//client.User.
	//	Query().
	//	Select(
	//		user.FieldID,
	//		user.FieldName,
	//		user.FieldAge,
	//	).
	//	Scan(ctx, &matched)
	//
	// endregion

	// With complicated where condition.
	//client.User.Query().Where(
	//	user.And(
	//		user.Or(
	//			user.NameEQ("aa"),
	//			user.NameEQ("bb"),
	//
	//			user.And(
	//				user.Age(10),
	//				user.HasCars(),
	//			),
	//		),
	//
	//		user.AgeEQ(18),
	//	),
	//)

	// Custom predicates can be useful if you want to write your own dialect-specific logic.
	//client.User.
	//	Query().
	//	Where(predicate.User(func(selector *sql.Selector) {
	//		selector.Where(sql.InInts(user.FieldAge, 1, 2, 3))
	//	})).
	//	AllX(ctx)

	// Pagination
	//r := client.User.
	//	Query().
	//	Offset(0).
	//	Limit(2).
	//	AllX(ctx)
	//
	//println(len(r))

	//// Group By
	//var v []struct {
	//	Name  string `json:"name"`
	//	Age   int    `json:"age"`
	//	Sum   int    `json:"sum"`
	//	Count int    `json:"count"`
	//}
	//
	//client.User.
	//	Query().
	//	GroupBy(user.FieldName, user.FieldAge).
	//	Aggregate(ent.Count(), ent.Sum(user.FieldAge)).
	//	ScanX(ctx, &v)
	//
	//println(len(v))

	//
	//client.User.Query().GroupBy(user.FieldName).Strings(ctx)

	//// Updates by id
	//client.User.
	//	UpdateOneID(u.ID).
	//	SetName("New Name").
	//	SetAge(18).
	//	Save(ctx)

	//// Updates by user.
	//u.Update().
	//	SetName("New Name").
	//	SetAge(18).
	//	Save(ctx)

	//// Deletes user.
	//client.User.DeleteOne(u).Exec(ctx)

	//// Deletes by id.
	//client.User.DeleteOneID(u.ID).Exec(ctx)

	if err != nil {
		return nil, fmt.Errorf("failed querying user: %v", err)
	}

	fmt.Printf("user returned: ", u)

	return u, nil
}

// Queries user with input name.
func QueryUserByName(ctx context.Context, client *ent.Client, name string) *ent.User {
	user, _ := client.User.Query().Where(user.NameEQ(name)).Only(ctx)

	return user
}

// Creates new cars and add them to a new user.
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
	// Creates a new car with model "Tesla".
	tesla, err := client.Car.
		Create().
		SetModel("Tesla").
		SetRegisteredAt(time.Now()).
		Save(ctx)

	if err != nil {
		return nil, fmt.Errorf("failed creating car: %v", err)
	}

	fmt.Println("car was created: ", tesla)

	// Create a new car with model "Ford".
	ford, err := client.Car.
		Create().
		SetModel("Ford").
		SetRegisteredAt(time.Now()).
		Save(ctx)

	if err != nil {
		return nil, fmt.Errorf("failed creating car: %v", err)
	}

	fmt.Println("car was created: ", ford)

	// Creates a new user, and adds it the 2 cars.
	a8m, err := client.User.
		Create().
		SetAge(30).
		SetName("a9m").
		AddCars(tesla, ford).
		Save(ctx)

	if err != nil {
		return nil, fmt.Errorf("failed creating user: %v", err)
	}

	fmt.Println("user was created: ", a8m)

	return a8m, nil
}

// Queries user's cars.
func QueryCars(ctx context.Context, a8m *ent.User) error {
	// Queries user's all cars.
	cars, err := a8m.
		QueryCars().
		Order(ent.Asc(car.FieldModel)).
		All(ctx)

	if err != nil {
		return fmt.Errorf("failed querying user cars: %v", err)
	}

	fmt.Println("returned cars:", cars)

	// What about filtering specific cars.
	ford, err := a8m.QueryCars().
		Where(car.ModelEQ("Ford")).
		Only(ctx)

	if err != nil {
		return fmt.Errorf("failed querying user cars: %v", err)
	}

	fmt.Println(ford)

	return nil
}

// Queries car's owner.
func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
	cars, err := a8m.QueryCars().All(ctx)

	if err != nil {
		return fmt.Errorf("failed querying user cars: %v", err)
	}

	// Queries the inverse edge.
	for _, ca := range cars {
		owner, err := ca.QueryOwner().Only(ctx)

		if err != nil {
			return fmt.Errorf("failed querying car %q owner: %v", ca.Model, err)
		}

		fmt.Printf("car %q owner: %q\n", ca.Model, owner.Name)
	}

	return nil
}

// Creates full graph for demo.
func CreateGraph(ctx context.Context, client *ent.Client) error {
	// First, creates the users.
	a8m, err := client.User.
		Create().
		SetAge(30).
		SetName("Ariel").
		Save(ctx)

	if err != nil {
		return err
	}

	neta, err := client.User.
		Create().
		SetAge(28).
		SetName("Neta").
		Save(ctx)

	if err != nil {
		return err
	}

	// Then, creates the cars, and attaches them to the users in the creation.
	_, err = client.Car.
		Create().
		SetModel("Tesla").
		SetRegisteredAt(time.Now()). // ignore the time in the graph.
		SetOwner(a8m). // attach this graph to Ariel.
		Save(ctx)

	if err != nil {
		return err
	}

	_, err = client.Car.
		Create().
		SetModel("Mazda").
		SetRegisteredAt(time.Now()). // ignore the time in the graph.
		SetOwner(a8m). // attach this graph to Ariel.
		Save(ctx)

	if err != nil {
		return err
	}

	_, err = client.Car.
		Create().
		SetModel("Ford").
		SetRegisteredAt(time.Now()). // ignore the time in the graph.
		SetOwner(neta). // attach this graph to Neta.
		Save(ctx)

	if err != nil {
		return err
	}

	// Creates the groups, and adds their users in the creation.
	_, err = client.Group.
		Create().
		SetName("GitLab").
		AddUsers(neta, a8m).
		Save(ctx)

	if err != nil {
		return err
	}

	_, err = client.Group.
		Create().
		SetName("GitHub").
		AddUsers(a8m).
		Save(ctx)

	if err != nil {
		return err
	}

	fmt.Println("The graph was created successfully")

	return nil
}

// Queries the cars belongs to GitHub group.
func QueryGithub(ctx context.Context, client *ent.Client) error {
	cars, err := client.Group.
		Query().
		Where(group.Name("GitHub")).
		QueryUsers().
		QueryCars().
		All(ctx)

	if err != nil {
		return fmt.Errorf("failed getting cars: %v", err)
	}

	println("cars returned:", cars)

	// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
	return nil
}

// Queries Ariel's cars.
func QueryArielCars(ctx context.Context, client *ent.Client) error {
	// Queris user with name 'Ariel' who has cars.
	a8m := client.User.
		Query().
		Where(
			user.HasCars(),
			user.Name("Ariel"),
		).
		OnlyX(ctx)

	// Queries cars which model is 'Mazda'.
	cars, err := a8m.
		QueryGroups().
		QueryUsers().
		QueryCars().
		Where(
			car.Not(
				car.ModelEQ("Mazda"),
			),
		).
		All(ctx)

	if err != nil {
		return fmt.Errorf("failed getting cars: %v", err)
	}

	println("cars returned:", cars)

	// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
	return nil
}

// Queries group which has users.
func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
	groups, err := client.Group.
		Query().
		Where(group.HasUsers()).
		All(ctx)

	if err != nil {
		return fmt.Errorf("failed getting groups: %v", err)
	}

	println("groups returned:", groups)

	// Output: (Group(Name=GitHub), Group(Name=GitLab),)
	return nil
}

func UpdateWithTransaction(ctx context.Context, client *ent.Client) error {
	tx, err := client.Tx(ctx)

	if err != nil {
		return fmt.Errorf("starting a transaction: %v", err)
	}

	_, err = tx.User.Create().SetName("").Save(ctx)

	if err != nil {
		return rollback(tx, fmt.Errorf("failed creating the group: %v", err))
	}

	// Works with transaction helper function.
	//err := WithTransaction(ctx, client, func(tx *ent.Tx) error {
	//	// Call any other functions which need ctx and tx.Client() here.
	//	// tx.Client() returns *ent.Client which associated transactional. We can use it with other functions accept *ent.Client.
	//
	//	return nil
	//})

	return tx.Commit()
}

// Rollbacks calls to tx.Rollback and wraps the given error
// with the rollback error if occurred.
func rollback(tx *ent.Tx, err error) error {
	if rerr := tx.Rollback(); rerr != nil {
		err = fmt.Errorf("%v: %v", err, rerr)
	}

	return err
}

// This is a transaction helper function.
func WithTransaction(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
	// Takes transactional client.
	tx, err := client.Tx(ctx)

	if err != nil {
		return err
	}

	// recover() function can detect any panic after any execution and roll back.
	// https://golangbyexample.com/recover-example-go/
	defer func() {
		if v := recover(); v != nil {
			tx.Rollback()
			panic(v)
		}
	}()

	// Calls input function and checks on the result.
	if err := fn(tx); err != nil {
		if rerr := tx.Rollback(); rerr != nil {
			//err = errors.Wrapf(err, "rolling back transaction: %v", rerr)
			err = errors.New(rerr.Error())
		}

		return err
	}

	// Tries to commit if no any error so far.
	if err := tx.Commit(); err != nil {
		//return errors.Wrapf(err, "committing transaction: %v", err)
		return errors.New(err.Error())
	}

	return nil
}

// This is a basic demo of how to exec raw sql.
// Check on https://golangdocs.com/mysql-golang-crud-example for more info.
func ExecuteRawSql(config AppConfig) {
	sqlHelper := shared.SqlHelper{}

	sqlHelper.WithDbConnection(func(db *sql.DB) {
		sql := `
			SELECT 
				id, 
				name, 
				age 
			FROM 
				users
			WHERE 
				age > ? AND 
				birthday IS NOT NULL
			`

		rows, err := db.Query(sql, 28)

		if err != nil {
			println(err.Error())
		}

		user := ent.User{}

		for rows.Next() {
			rows.Scan(&user.ID, &user.Name, &user.Age)

			println(user.Name)
		}
	}, "", config.DataSource.Dsn)
}
